其他计算器模块
计算器的其他模块也可以用类似分析器的方式组织起来。然而,由于这些模块非常小,它们就没必要再有自己的 _impl.h
文件。只有在逻辑模块由许多函数组成,而它们又需要一个共享环境时,才需要使用这种头文件。
错误处理器已经缩减为一组异常类型,根本不需要error.c:
error.h:
namespace Error {
struct Zero_divide { };
struct Syntax_error {
const char* p;
Syntax_error(const char* q) { p = q; }
};
}
词法处理器提供的是一个相当大的而且比较污浊的界面:
// lexer.h:
namespace Lexer {
enum Token_value {
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='\',
PRINT=';', ASSIGN='=', LP='(', RP=')'
};
extern Token_value curr_tok;
extern double number_value;
extern std::string string_value;
Token_value get_token();
}
除了 lexer.h
外,词法处理器的实现还依赖于 error.h
、<iostream>
,以及在 <cctype>
里声明的那些用于确定字符类别的函数:
// lexer.c:
#include "lexer.h"
#include "error.h"
#include <iostream>
#include <cctype>
Lexer::Token_value Lexer::curr_bok;
double Lexer::number_value;
std::string Lexer::string_value;
Lexer::Token_value Lexer::get_token(){ /* ... */ }
我们也可以将对于 error.h
的#include指令提取出来,放入一个lexer的 _impl.h
文件里。当然,我认为对这种小程序而言,那样做实在太过分了。
与平常一样,我们将这个模块提供的界面(在这里是lexer.h)用#include包含到模块的实现中,以使编译器能做一致性检查。
符号表基本上是自足的,因为标准库头文件 <map>
能够带来与之相关的所有东西,那里实现了一个有效的map模板类:
// table.h:
#include <map>
#include <string>
extern std::map<std::string, double> table;
由于我们假定每个头文件都可能通过#include包含到几个 .c
文件里去,我们必须将table的声明与其定义分开,虽然table.c和table.h的差别仅在一个关键字extern:
// table.c:
#include "table.h"
std::map<std::string, double> table;
简而言之,驱动程序依赖于所有的东西:
// main.c:
#include "parser.h"
#include "lexer.h"
#include "error.h"
#include "table.h"
namespace Driver {
int no_of_errors;
std::istream* input;
void skip();
}
#include <sstream>
int main(int argc, char* argv[]){ /* ... */ }
由于Driver名字空间只是由main()使用,因此我将它放在main.c里。我也可以换另一种方式,将它分出来做成一个driver.h,并用#include包含进来。
对于更大的系统,通常值得考虑进一步的组织,使驱动程序直接依赖的东西更少一些。也经常值得将main()所做的事情最小化,让main()只是去调用一个位于另一个独立源文件里的驱动函数。对于想作为库去使用的代码,这样做就特别重要。因为此后我们将不能依靠main()里的代码,而必须为来自各种各样函数的调用做好准备(9.6[8])。
🔚